Hintaennustin tehtiin Python-koodilla Jupyter Notebooks -työkalun avulla. Myös dokumentaation kirjoitin suoraan Jupyter Notebook -tiedostoon Markdown-soluihin. Kehitysympäristöksi asensin omaan kannettavaan tietokoneeseeni Jupyter Notebooksin sisältävän Anaconda-alustan (2019-10, Anaconda Navigator 1.9.7). Anaconda on erityisesti datatieteeseen ja koneoppiseen soveltuva ja runsaasti käytetty avoin Python- ja R-alusta, jota on helppo laajentaa Conda-paketinhallintatyökalulla. Anaconda Navigator on Anaconda-alustan graafinen käyttöliittymä, jolla voi luoda ja hallita erillisiä ympäristöjä sekä käynnistää valittuun ympäristöön asennettuja sovelluksia. Jupyter Notebook on selaimella käytettävä web-sovellus. Sitä käytetään aina selaimesta siitä riippumatta, onko sen palvelin paikallisella tietokoneella vai verkon takana.
Käyttöjärjestelmänä on Ubuntu 18.04 Linux, ja harjoitustyön julkaisualustana on GitHub-repositorio. Editorina käytin Visual Studio Codea ja vanhaa kunnon Emacsia, joskaan niitä ei juurikaan tarvinnut, sillä lähes kaiken sai kätevästi tehtyä suoraan Jupyter Notebookilla. Asentamani Anaconda-distribuution Pythonin versio on 3.7.4 ja Jupyter Notebookin versio on 6.0.1.
Asennuksessa tuli vähän sekoiltua, kun asensin ensin Pythonin uuden version suoraan ja sain koko käyttöjärjestelmän sekaisin määrittelemällä tämän uuden Python-tulkin oletustulkiksi: terminaaliohjelmakaan ei enää käynnistynyt. Tämä johtui siitä, että Linux-käyttöjärjestelmässä käynnistetään kaikenlaista Pythonin avulla ja tämä koodi on vanhempaa Pythonia, joten kaikki ei enää käynnistynyt käyttöjärjestelmässä kuten pitäisi. Tämä asennus oli kaiken lisäksi ihan turha, sillä Anacondan mukana tulee tarvittava Python-ympäristökin. Poistin tuhan asennuksen ja asensin Anacondan ohjeiden mukaisesti. Linuxiin asentaminen ei ole ihan niin suoraviivaista kuin Windowsiin, mutta netistä löytyneiden ohjeiden avulla se sujui kuitenkin kohtuullisen kivuttomasti.
Toinen varteenotettava vaihtoehto olisi ollut käyttää Jypyter Notebook -sovellusta CSC Notebooks -pilvipalvelun aikarajoitetutussa virtuaalikoneessa kuten koodiklinikalla tehtiin. Päädyin kuitenkin asentamaan tarvittavat ohjelmat omaan kannettavaan tietokoneeseeni, sillä sitä olisin kuitenkin käyttänyt, vaikka sovellusta olisikin ajettu verkon yli. Tämän vuoksi helpointa oli tehdä kaikki suoraan paikallisesti omalla koneella, ja näin ympäristökin jää minulle talteen.
Päätin käyttää Inside Airbnb -datasettiä. Olisi ollut mielenkiintoisempaa tutkia jotain muuta data-aineistoa, mutta kurssi oli pakko jo saada pakettiin mahdollisimman pian ja siksi päätin valita tämän valmiin ja harjoituksista tutun datasetin. Tutkittavaksi kaupungiksi valitsin Wienin, sillä kaikki minulle tutummat kaupungit oli jo valittu tutkittavaksi. Minulle tutummista Budapestistä ja Etelä-Ranskan kaupungeista ei ollut data-aineistoa tässä datasetissä, enkä halunnut valita myöskään harjoituksissa käsiteltyä Berliiniä. Wien oli ainoa käsittelemätön kaupunki, josta minulla oli ennestään edes joku käsitys.
Inside Airbnb -datasetissä on Wienistä tallennettuna tiedostot listings.csv.gz, calendar.csv.gz, reviews.csv.gz, listings.csv, reviews.csv, neighbourhoods.csv ja neighbourhoods.geojson. Näistä ensimmäinen on mielenkiintoisin sisältäen yksityiskohtaista tietoa varauskohteista, toinen sisältää yksityiskohtaisen varauskalenterin ja kolmas kohteiden tekstimuotoisia arviointeja. Kaksi pakkaamatonta tiedostoa ovat vastaavien pakattujen tiedostojen yhteenvetoja ja viimeinen tiedosto sisältää tiedostossa neighbourhoods.csv lueteltujen kaupunginosien paikkatietoja. Dataa oli yli 10 tuhannesta kohteesta, joten sitä oli riittävästi, ja tiedot oli päivitetty viimeksi 19.11. 2019, joten data lienee ajantasaista ja luotettavaa.
Harjoitustyön pohjana käytin Nick Amaton blogia Airbnb price predictor. Ihan ensimmäisenä otetaan käyttöön datan keräämiseen, jalostamiseen, kuvailemiseen sekä koneoppimisessa tarvittavia kirjastoja. Tutustuin aluksi tiedostojen tiedostot listings.csv.gz, calendar.csv.gz, reviews.csv.gz sisältöihin lukemalla ne Pandas-kirjaston dataframeiksi. Listings-dataframe sisälsi yhteensä 106 eri saraketta kohdeindeksi mukaan lukien. Reviews-dataframessa ainoa varsinainen tietosarake sisälsi tekstimuotoisia arvosteluja, joiden hyödyntäminen olisi ollut varsin vaikeaa tässä työssä. En myöskään kokenut tarpeelliseksi käyttää Calendar-dataframen tietoja hintaennustimessa, sillä sen sisältämät tulevaisuuden varaustiedot ja hintapyynnöt eivät tuntuneet ennusteen kannalta relevanteilta selittäjiltä. Tämän vuoksi päätin käyttää ennustimessa vain listings-tietoja ja pitäytyä alkuperäisen esimerkin sarakevalinnoissa, mutta karttavisualisointien vuoksi otin mukaan myös sarakkeet 'latitude' ja 'longitude' sekä kohteen Airbnb-linkin, jonka avulla valittua kohdetta voi tarkastella yksityiskohtaisesti. Lisäksi otin mukaan avainkentän ’id’ siltä varalta, että haluaisin kuitenkin myöhemmin hyödyntää calendar- tai reviews-tietoja. Otin talteen myös aineistossa olevan geojson-tiedoston wep-osoitteen, sillä käytän sitä karttavisualisoinneissa.
Halusin harjoitustyön lopuksi tehdä jonkinlaisen vuorovaikutteisen dashboardin Power BI -ohjelmistolla, mutta sen karttavisualisointi ei löytänyt kaikkia kaupunginosia pelkän nimen perusteella. Tämä johtunee kaupunginosien nimien sisältämistä umlaut- ja ß-merkeistä, jotka muutenkin aiheuttivat harmia pitkin harjoitustyötä. Wienissä käytetään yleisemmin kaupunginosanumeroita kuin niiden nimiä (esimerkiksi Innere Stadt on 1. Bezirke). Power BI -ohjelman karttavisualisointi löysikin kaupunginosat tämän kaupunginosanumeron perusteella. Tämän vuoksi lisäsin kaupunginosanumerot kohteet sisältävään dataframeen. Tein tämän raapimalla kaupunginosanumerot ja -nimet Wienin kaupungin web-sivuilta. Raavituista tiedoista muodostin ensin oman dataframen ja lopuksi yhdistin dataframet yhteen Pandaksen join-operaatiolla. Näin tuli kokeiltua oman datan keräämistäkin joskin varsin minimaalisella esimerkillä.
import pandas as pd
import requests
from bs4 import BeautifulSoup
import numpy as np
import folium # conda install folium -c conda-forge
from folium.plugins import HeatMap
from folium.plugins import MarkerCluster
import urllib
import json
from sklearn import impute
from sklearn import ensemble
from sklearn import linear_model
from sklearn.model_selection import learning_curve,GridSearchCV
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
import sklearn.metrics as metrics
import matplotlib.pyplot as plt
from collections import Counter
folder = 'http://data.insideairbnb.com/austria/vienna/vienna/2019-11-19/data/'
listings_file = folder + 'listings.csv.gz'
calendar_file = folder + 'calendar.csv.gz'
reviews_file = folder + 'reviews.csv.gz'
folder = 'http://data.insideairbnb.com/austria/vienna/vienna/2019-11-19/visualisations/'
geojson_file = folder + 'neighbourhoods.geojson'
df_c = pd.read_csv(calendar_file, compression='gzip')
df_c.head()
df_r = pd.read_csv(reviews_file, compression='gzip')
df_r.head(2)
df = pd.read_csv(listings_file, compression='gzip')
print(df.columns[0:50]) # tai print(pd.Series(df.columns).head(50))
print(df.columns[50:106])
len(df)
cols = ['id',
'price',
'accommodates',
'bedrooms',
'beds',
'neighbourhood_cleansed',
'room_type',
'cancellation_policy',
'instant_bookable',
'reviews_per_month',
'number_of_reviews',
'availability_30',
'review_scores_rating',
'latitude',
'longitude',
'listing_url'
]
# read the file into a dataframe
df = pd.read_csv(listings_file, compression='gzip', encoding='utf-8', usecols=cols)
print(df.head())
Tarkastellessani dataframen sisältöä, huomasin, että osa sarakkeen ’ neighbourhood_cleansed’ merkeistä jää tulostumatta oikein. Tämän ongelman selvittämiseksi ja korjaamiseksi piti tehdä vähän lisätarkasteluja.
df.neighbourhood_cleansed.unique()
df1 = pd.read_csv(listings_file, compression='gzip', encoding='utf-8',
usecols=['neighbourhood'])
df1.neighbourhood.unique()
Tarkasteltuani muiden sarakkeiden sisältöjä huomasin, että sarakkeen ’ neighbourhood_cleansed’ merkit on koodattu eri tavalla kuin muiden sarakkeiden merkit. Muualla koodaus oli utf-8, mutta sarakkeessa ’ neighbourhood_cleansed’ oli varsin epämääräinen koodaus. Koodaus vaikutti olevan muuten utf-8, mutta umlaut-merkien ja ß -merkin jälkimmäinen tavu näytti olevan mac_roman-koodauksen vastaava tavukoodi. Korjasin nämä virheelliset merkit metodin replace avulla, minkä jälkeen sarakkeen ’neighbourhood_cleansed’ näytti taas järkevältä ja kaupunginosien nimet tulostuivat siinä oikein.
Tämän asian ymmärtämiseen kului käsittämättömän paljon aikaa, sillä ensin luulin koko tiedoston merkkikoodauksen olevan joku erikoisempi, mutta mitään sopivaa ei löytynyt. Selitys löytyi vasta, kun aloin tutkia merkkijonojen tavusisältöä. Sitäkään tehdessä ei ensimmäisenä tule mieleen, että koodaus mennyt sekaisin, mutta mitään muuta selitystä en keksinyt. Muidenkin saksakielisten kaupunkien tiedostoja katselin enkä löytänyt niistä samaa ongelmaa.
df['neighbourhood_cleansed'] = \
df['neighbourhood_cleansed'].replace(['\u008A','\u009A','\u009F','\u0080',
'\u0085','\u0086','\u00A7'],
['ä','ö','ü','Ä','Ö','Ü','ß'], regex=True)
df.neighbourhood_cleansed.unique()
Sitten raavitaan kaupunginosanumerot Wienin kaupungin web-sivuilta ja lisätään ne dataframeen, jotta voidaan lopuksi tehdä karttavisualisointi Power BI:lla.
response = requests.get('https://www.vienna.at/features/bezirke-wien') # read the web page
soup = BeautifulSoup(response.content, 'html.parser')
# Convert to string
pretty = soup.prettify()
#print(pretty)
bezirkes = soup.select('ul[id=bezirke] li', limit = 23) # Soup object type,
# total 23 bezirkes in Vienna
ordernumbers = []
names = []
addresses = []
# Go through all bezirkes in the list
for bezirke in bezirkes:
# Get bezirke order number, name and address
bezirkeData = bezirke.text.split('\n')
ordernumbers.append(bezirkeData[1])
names.append(bezirkeData[2])
addresses.append(bezirkeData[3])
print(ordernumbers, names, addresses)
# Create a pandas dataframe
df_bezirkes = pd.DataFrame({'BezirkeNbr':ordernumbers, 'Bezirke':names, 'Zip':addresses})
df_bezirkes.head(23)
df_bezirkes.to_csv('bezirkes.csv', index=False, encoding='utf8') # save it (just for a case)
Lopuksi yhdistetään raavitusta datasta muodostettu kaupunginosanumerot sisältävä dataframe kohteet sisältävään dataframeen.
# join this dataframe to the listing datafarame
df = df.join(df_bezirkes.set_index('Bezirke'), on = 'neighbourhood_cleansed')
df.head(2)
Talletetaan dataframe tiedostoon, jotta sen sisältöä voi halutessaan tarkastella myös muilla työkaluilla (esimerkiksi taulukkolaskentaohjelmalla).
df.to_csv('data.csv', index='False')
len(df.index)
Kenttä ’ neighbourhood_cleansed’ sisältää kohteen kaupunginosan osan nimen. Kun tarkastellaan sen jakaumaa, havaitaan jakauman olevan Wienin datassa yllättävän tasainen: vain yhdessä kaupunginosassa on alle 100 kohdetta ja enimmillään kohteita on 1300. Kaupunginosia on yhteensä 23, joten kaupunginosajakokaan ei ole liian tarkka. Tämä kenttä vaikuttaa tässä aineistossa varsin käyttökelpoiselta, sillä eri kaupunginosista on varsin mukavasti rivejä. Esimerkissä käyytetty San Franciscon data oli paljon epätasaisemmin jakautunut.
nb_counts = Counter(df.neighbourhood_cleansed)
tdf = pd.DataFrame.from_dict(nb_counts, orient='index').sort_values(by=0)
# Redefining visualization width
plt.rcParams["figure.figsize"] = [20, 5]
tdf.plot(kind='bar')
len(nb_counts)
df_nb = pd.DataFrame.from_dict(nb_counts, orient='index', columns=['count'])
df_nb.columns
df_nb.sort_values(by=['count'], ascending=False).head(12)
Dataa tuli jalostettua jo edellisessä tarkasteluvaiheessa, kun korjasin kentän 'neighbourhood_cleansed' merkkijonokoodauksen. Kyseinen toimenpide oli kuitenkin lähinnä kosmeettinen; sen ainoa toiminnallinen merkitys liittyi siihen, että sitä käytettiin avainkenttänä yhdistettäessä kaksi datalähdettä. Tällaisia kosmeettisilta vaikuttavia jalostustoimenpiteitä ei kannata kuitenkaan aliarvioida, sillä datan esittäminen oikeassa muodossa on tärkeää ymmärrettävyydenkin vuoksi, kun dataa tutkitaan esimerkiksi visualisointien avulla. Tätä dataa täytyy toki hieman siivota ja jalostaa myös varsinaisista toiminnallisista syistä ennen kuin sitä kannattaa tarkemmin visualisoida ja ennen kuin sen avulla voi opettaa mallia. Puuttuvat arvot pitää imputoida tai puuttuvia arvoja sisältävät rivit pitää poistaa ja kategoriset (ei-numeeriset) arvot pitää korvata numeerisilla arvoilla.
Sarakkeessa 'reviews_per_month' on paljon puuttuvia arvoja. Tarkasteltaessa sarakkeiden 'number_of_reviews' ja 'reviews_per_month' arvoja havaitaan, että sarakkeessa 'reviews_per_month' on NaN-arvo ainoastaan silloin, kun sarakkeessa 'number_of_reviews' on nolla. Näin ollen sarakkeen 'reviews_per_month' NaN-arvot voidaan muuttaa nolliksi. Näin myös myöhemmin tehdään. Ensimmäinen alla olevista lausekkeista tarkistaa, ettei 'reviews_per_month' ole koskaan muuta kuin NaN silloin kun 'number_of_reviews' on nolla. Toinen lausekkeista tarkistaa, ettei 'reviews_per_month' ole koskaan NaN silloin kun 'number_of_reviews' poikkeaa nollasta. NaN-arvot ja arvioiden puuttuminen ovat siis ekvivalentit.
# the number of entries with 0 'number_of_reviews' which do not a NaN for 'reviews_per_month'
len(df[((df.number_of_reviews == 0) & (pd.isnull(df.number_of_reviews) == False)
& (pd.isnull(df.reviews_per_month) == False))].index)
# the number of entries with at least 1 'number_of_reviews' which have a NaN for 'reviews_per_month'
len(df[(df.number_of_reviews != 0) & (pd.isnull(df.number_of_reviews) == False)
& (pd.isnull(df.reviews_per_month) == True)].index)
Korvataan kaikki NaN-arvot arvolla 0 sarakkeessa 'reviews_per_month', koska näistä kohteista ei ole arvioita. Lisäksi poistetaan aineistosta sellaiset epäilyttävät kohteet, joissa ei ole makuuhuoneita (0 sarakkeessa 'bedrooms') tai vuoteita (0 sarakkeessa 'beds'). Myöhemmin poistetaan myös kohteet, joiden hinta on 0, mutta hintasarakkeesta pitää poistaa $-merkki ja se pitää muuttaa numeeriseksi ennen kuin 0-hintaiset kohteet kannattaa poistaa. Alkuperäisessä esimerkissä tämä hintasuodatus tehty väärin jo tässä kohdassa ilman $-merkkiä, eikä se siten poista 0-hintaisia kohteita. Lopuksi poistetaan vielä aineistosta kaikki kohteet, joihin on jäänyt NaN-arvo johonkin sarakkeeseen. Kaikkia kohteita ei olisi välttämätöntä poistaa, vaan voitaisiin myös yrittää imputoida puuttuvia arvoja muiden kohteiden arvojen avulla. Esimerkiksi sarakkeen 'review_scores_rating' NaN-avot voitaisiin imputoida vaikkapa sen muista kohteista laketulla keskiarvolla. Näin saataisiin muiden sarakkeiden suurempi aineisto ennustusmallinopetukseen, mutta visalisointiin tulisi näkyviin tekaistuja arviointintilukemia, mikä ei ole suotavaa.
# so we need to do some cleaning.
# first fixup 'reviews_per_month' where there are no reviews
df['reviews_per_month'].fillna(0, inplace=True)
# just drop rows with bad/weird values
# (we could do more here)
df = df[df.bedrooms != 0]
df = df[df.beds != 0]
#df = df[df.price != 0] # TÄTÄ EI VOI TEHDÄ VIELÄ TÄSSÄ, sillä ensin on poistettava $-merkit
# 'review_score_rating' NaN-arvot voisi myös imputoida esimerkiksi näin:
imputer = impute.SimpleImputer(strategy='median')
#df.review_scores_rating = imputer.fit_transform(df.review_scores_rating.values.reshape(-1, 1)).reshape(-1, 1)
df = df.dropna(axis=0) # poistetaan kuitenkin kaikki rivivit, joissa on jäljellä NaN-arvoja
# tarkastellaan kaikkia asuntoja toisin kuin alkuperäisessä esimerkissä
len(df.index)
df.price.head(5)
# remove the $ from the price and convert to float
df['price'] = df['price'].replace('[\$,)]','', \
regex=True).replace('[(]','-', regex=True).astype(float)
df.price.sort_values(ascending=False).head(61)
df.price.sort_values(ascending=True).head(100)
Nyt $-merkit on poistettu hintakentästä ja se on muutettu liukuluvuksi, joten dataframesta voidaan poistaa ne rivit, joista hinta puuttuu. Näitä tosin ei näytä olevan tässä aineistossa. Aineistossa näyttää olevan 60 kohdetta, joiden hinta on yli 500. Poistetaan nämä kalliit asunnot dataframesta, koska nämä eivät kiinnostane normaalia asunnon vuokraajaa ja jotta jakaumista saadaan tehtyä informatiivisempia kuvia. Tarkastellaan kuitenkin ensin, missä kalliit asunnot sijaitsevat. Alla olevasta listauksesta nähdään, että kalliita asuntoja on yllättäen ympäri kaupunkia, joskin ne painottuvat eskusta-alueelle. Niiden merkitys on kuitenkin varsin vähäinen analysoitaessa kaupunginosien hintatasoja.
df[df.price > 500].groupby(
['neighbourhood_cleansed']
).agg(
count=('id', len)
).sort_values(by=['count'], ascending=False)
# Nyt voi tehdä hintaan perustuvat suodatukset
df = df[df.price != 0] # turha Wien-aineistossa 19.11.2019
df = df[df.price <= 500]
len(df)
# Talletetaan siivottu data PowerBI:ta varten visualisoitavaksi
df.to_csv('data_windows.csv', encoding='windows-1252')
Osa mukaan valituista piirteistä on kategorisia, joten niiden avulla ei voi suoraan opettaa ennustemallia. Joukossa on myös pelkästään visualisointia varten mukaan otettuja muuttujia, jota voidaan tiputtaa pois dataframesta ennen opettamista. Tällaisia ovat esimerkiksi kohteen url sekä kaupunginosanumero ja postiosoite.
Selittäjiksi suunnitellut kategoriset piirteet pitää kuitenkin muuttaa numeerisiksi. Merkkijonomuotoisen kentän 'neighborhood_cleansed' tietosisältö saadaan muutettua numeeriseksi ns. dummy-muuttujien avulla käyttämällä Pandaksen funktiota get_dummies. Menetelmää kutsutaan nimellä "one hot encoding". Siinä jokaista sarakkeen merkkijonoarvoa kohden muodostetaan uusi sarake. Kunkin rivin merkkijonoa vastaavaan uutteen sarakkeeseen sijoitetaan arvo 1 ja kaikkiin muihin uusiin muodostettuihin sarakkeisiin sijoitetaan arvo 0. Samaa menetelmää käytetään myös merkkijonomuotoisille kentille 'room_type' ja 'cancellation_policy'.
Kenttä 'instant_bookable' on myös muodollisesti merkkijono, mutta sillä on vain kaksi arvoa 't' ja 'f', joten tosiasiallisesti se kuvaa totuusarvoa. Siitä generoidaan myös ensin uudet sarakkeet samalla menetelmällä, mutta muodostuviin lisätään prefix 'instant'. Koska muodostuvat kaksi saraketta ovat toistensa vastakohdat, poistetaan lopuksi arvoja epätosi edustava sarake. Kaikki funktiolla get_dummies muodostetut sarakkeet pitää lopuksi lisätä piirteet sisältävään dataframeen (alldata), ja niitä vastaavat kategoristen muuttujien sarakkeet pitää vastaavasti tiputtaa pois dataframesta.
# get feature encoding for categorical variables
n_dummies = pd.get_dummies(df.neighbourhood_cleansed)
rt_dummies = pd.get_dummies(df.room_type)
xcl_dummies = pd.get_dummies(df.cancellation_policy)
# convert boolean column to a single boolean value indicating whether this listing has instant booking available
ib_dummies = pd.get_dummies(df.instant_bookable, prefix="instant")
ib_dummies = ib_dummies.drop('instant_f', axis=1)
# replace the old columns with our new one-hot encoded ones
alldata = pd.concat((df.drop(['neighbourhood_cleansed', \
'room_type', 'cancellation_policy', 'instant_bookable'], axis=1), \
n_dummies.astype(int), rt_dummies.astype(int), \
xcl_dummies.astype(int), ib_dummies.astype(int)), \
axis=1)
allcols = alldata.columns
alldata.head(5)
Ensimmäiseksi kuvailin asuntojen hintoja alueittain boxplot-kaavion avulla. Kaavion luettavuuden vuoksi siihen ei voi ottaa mukaan kaikkia 23 kaupunginosaa. Ei myöskään ole mielekästä tarkastella samalla asteikolla 50 ja yli 500 kohteen kaupunginosia. Kaaviota varten muodostetaan ensin indeksitaulukko 'top_neighbourhoods', johon otetaan mukaan ne kaupunginosat, joissa on alkuperäisessä aineistossa yli 500 vuokrauskohdetta. Indeksitaulukko lajitellaan, ja sen avulla poimitaan siivotusta (kategoriset muuttujat sisältävästä) dataframesta näiden suositumpien kaupunginosien kohteet dataframeen 'df_top'. Tästä dataframesta piirretään boxplot-kaavio kaupunginosien hintatasosta kirjaston Seaborn funktiolla boxplot. Kaaviosta nähdään, että vuokrauskohteiden hinnat ovat jakautuneet varsin samalla tavalla kaikissa muissa paitsi 1. kaupunginosassa Innere Stadt, eli vain aivan ydinkeskustan kohteet ovat selvästi muita kalliimpia.
# Some kind of a boxplot
df.price.value_counts()
top_neighbourhoods= df_nb[df_nb['count'] > 500].sort_values(by=['count'],ascending=False).index
df_top = df[df.neighbourhood_cleansed.isin(top_neighbourhoods)]
df_top.head()
import seaborn as sns
AX = sns.boxplot(x='neighbourhood_cleansed', y='price', data=df_top)
Seuraavaksi oli karttavisualisointien vuoro, sillä asuntokohteiden tarkastelussa asunnon sijainti ja sen ympäristö kiinnostavat yleensä erittäin paljon vuokraajaa. Päätin käyttää näihin visualisointeihin Teppo Kivennon löytämää kirjastoa Folium. Kirjastoa ei ollut valmiiksi Anacondassa, joten asensin sen oheistuksen mukaisesti (conda install folium -c conda-forge). Netistä löytyi aika paljon esimerkkejä tämän kirjaston käyttämisestä (esimerkiksi Quickstart, kaggle ja Medium). Toteutin Folium-kirjaston avulla kohteiden lämpökartan, kohdekartan ja kaupunginosakartan. Kohdekartassa käytetään kohdeklusterointia ryhmittelemään yksittäisiä kohteita eri zoomaustasoilla. Klusterointi muuttuu automattisesti zoomatessa.
Lämpökartta antaa aika karkean kuvan kohteista, mutta zoomaamalla siitäkin saa jotain informaatiota. Lämpökartta talletetaan tiedostoon heat_map.html. Klusteroidun kohdekartan avulla on helpompi hahmottaa kohteiden sijaiti ja niitä pääsee myös tarkastelemaan. Liitin kohteisiin popup-tulosteet, jotka sisältävät kohteen hinnan, majoituskapasiteetin ja Airbnb-linkin. Kun linkkiä klikkaa popupista, avautuu kohteen Airbnb-sivu uuteen selaimen välilehteen. Chrome ei tulostanut kuvaa Jupyterissa, jos siinä on yli 2000 popup-tulostetta, mutta talletetusta html-tiedostosta (listing_map.html) se generoi kuvan erilliseen ikkunaan. Firefoxissa kuva tulostui myös Jupyter Notebookissa, vaikka kaikkiin kohteisiin liittyi popup. Tämän seurauksena siirryin käyttämään Firefoxia Jupyter Notebookin kanssa.
Kohdekartan lisäksi tein foliumilla myös karttavisualisoinnin kaupunginosista Inside Airbnb -datasetin sisältämän geojson-datan avulla. Valitettavasti tässäkin tiedostossa oli taas kaupunginosien nimissä sama kummallinen merkkikoodaus, ja jouduin taas konvertoimaan merkkijonot. Määritin kaupunginosille eri värit, jotta ne erottuvat selkeästi kartassa. Lisäsin karttaan jokaisen kaupunginosan kohdalle kohteiden yhteenvetotietoja sisältävän popup-tulosteen. Popup-tuloste sisältää kohteiden keskihinnan, arvioiden keskiarvon sekä kohteiden lukumäärän yhteensä ja kohdetyypeittäin. Tämän kartan avulla saa yleiskatsauksen kohteiden hintatasosta ja lukumääristä eri puolilla Wieniä ja voi tarkastella kaupunginosien välisiä eroja. Tallensin tämän kartan tiedostoon bezirke_map.html.
Tarkastelin myös Wienin kaupungin avointa dataa ja totesin, että tarjolla oli runsaasti erilaisia paikkatietoja sisältäviä datalähteitä. Koska Wienissä on valtavasti erilaisia museoita, päätin kokeilla lisätä kaupunginosakarttaan löytämäni museolistauksen kohteet. Generoin tämän kartan uusteen ikkunaan, sillä merkintöjä oli niin paljon, ettei keskikaupungin kaupunginosiin pystynyt enää kohdistamaan osumatta museomerkintään, jollei zoomannut lähemmäksi. Museoista kiinnostunut asunnon vuokraaja voi valita asunnoin kaupunginosan tämän kartan avulla. Tämä kartta löytyy talletettuna tiedostosta museum_map.html. Kartat tulostuvat Jupyter Notebooksissa vain sillä istuntokerralla, jolla ne generoidaan. Ne eivät näy myöskään GitHubissa avattaessa Jupyter Notebook -tiedosto. Edellä mainituista tiedostoista kartat ovat suoraan ladattavissa selaimeen, ja karttatiedostot ovat myös ladattavissa GitHubista. Suosittelen käyttämään Firefoxia.
map = folium.Map(location=[48.210033, 16.363449], zoom_start = 12)
#folium.TileLayer("OpenStreetMap").add_to(map)
coordinates = np.array([alldata.latitude.to_list(), alldata.longitude.to_list()]).T
# transpose matrix
HeatMap(coordinates).add_to(map)
map.save('heat_map.html')
map
map = folium.Map(location=[48.210033, 16.363449], zoom_start = 12)
marker_cluster_group = MarkerCluster(name="Airbnb").add_to(map)
for row in alldata.iterrows():
text = ('Price: ' + str(row[1].price) + '<br>' +
'Accommodates: ' + str(row[1].accommodates) + '<br>' +
'<a href =\"' + str(row[1].listing_url) + '\" target=\"_blank\">' +
str(row[1].listing_url) + '</a></p>'
)
folium.Marker(location=[row[1].latitude, row[1].longitude],
popup = folium.Popup(text, max_width=250)).add_to(marker_cluster_group)
marker_cluster_group.add_to(map)
map.save('listing_map.html')
map